<?php
/**
 * @file
 */

/**
 * Implements of hook_entity_info()
 */
function nodejschat_entity_info() {
  $return = array(
    'nodejschat' => array(
      'label' => t('NodejsChat'),
      'controller class' => 'NodejsChatController',
      'base table' => 'nodejschat',
      'uri callback' => 'nodejschat_uri',
      'fieldable' => TRUE,
      'entity keys' => array(
        'id' => 'cid',
      ),
      'bundles' => array(
        'nodejschat' => array(
          'label' => t('NodejsChat'),
          'admin' => array(
            'path' => 'admin/config/nodejschat/nodejschat',
            'access arguments' => array('administer nodejschat'),
          ),
        ),
      ),
      'view modes' => array(
        'full' => array(
          'label' => t('NodejsChat'),
          'custom settings' => FALSE,
        ),
      ),
    ),
  );
  return $return;
}

/**
 * Implements callback_entity_info_uri().
 */
function nodejschat_uri($nodejschat) {
  return array(
    'path' => 'nodejschat/' . $nodejschat->cid,
    'options' => array('fragment' => 'nodejschat-' . $nodejschat->cid),
  );
}

/**
 * Prepares a nodejschat object for editing.
 *
 * Fills in a few default values, and then invokes hook_prepare() on the nodejschat
 * type module, and hook_nodejschat_prepare() on all modules.
 *
 * @param $nodejschat
 *   A nodejschat object.
 */
function nodejschat_object_prepare($nodejschat) {
  // Set up default values, if required.
  // If this is a new nodejschat, fill in the default values.
  if (!isset($nodejschat->cid) || isset($nodejschat->is_new)) {
    global $user;
    $nodejschat->uid = $user->uid;
    $nodejschat->created = REQUEST_TIME;
    $nodejschat->sid = session_id();
  }
  else {
    $nodejschat->created = REQUEST_TIME;
  }
  $settings = variable_get('nodejschat_default_settings', array());
  // Set the title fields.
  if (empty($nodejschat->title)) {
    // Build title
    $anon_name = token_replace(t($settings['anon_name']));
    $nodejschat->title = $anon_name;
  }
  // Set the format fields.
  if (empty($nodejschat->format)) {
    $nodejschat->format = $settings['msg_format'];
  }
  // Set the public fields.
  if (!isset($nodejschat->public)) {
    $nodejschat->public = TRUE;
  }
  // Set the status fields.
  if (empty($nodejschat->status)) {
    $nodejschat->status = NODEJSCHAT_WAITING;
  }
  // Set the timestamp fields.
  if (empty($nodejschat->created)) {
    $nodejschat->created = REQUEST_TIME;
  }

  module_invoke_all('nodejschat_prepare', $nodejschat);
}

/**
 * Saves changes to a node or adds a new node.
 *
 * @param $nodejschat
 *   The $nodejschat object to be saved. If $nodejschat->cid is
 *   omitted (or $nodejschat->is_new is TRUE), a new node will be added.
 */
function nodejschat_save($nodejschat) {
  $transaction = db_transaction();

  nodejschat_object_prepare($nodejschat);

  try {
    field_attach_presave('nodejschat', $nodejschat);

    // Determine if we will be inserting a new node.
    if (!isset($nodejschat->is_new)) {
      $nodejschat->is_new = empty($nodejschat->cid);
    }

    if (!isset($nodejschat->created)) {
      $nodejschat->created = REQUEST_TIME;
    }
    // The changed timestamp is always updated for bookkeeping purposes,
    // for example: revisions, searching, etc.
    $nodejschat->changed = REQUEST_TIME;
    $nodejschat->timestamp = REQUEST_TIME;

    // Let modules modify the node before it is saved to the database.
    module_invoke_all('nodejschat_presave', $nodejschat);
    module_invoke_all('entity_presave', $nodejschat, 'nodejschat');

    // Save the node and node revision.
    if ($nodejschat->is_new) {
      // For new nodejschat, save new records for the nodejschat itself
      global $user;
      $nodejschat->uid = $user->uid;
      drupal_write_record('nodejschat', $nodejschat);
      $op = 'insert';
    }
    else {
      // For existing nodejschat, update the nodejschat record which matches the value of
      // $nodejschat->cid.
      drupal_write_record('nodejschat', $nodejschat, 'cid');
      $op = 'update';
    }

    // Save fields.
    $function = "field_attach_$op";
    $function('nodejschat', $nodejschat);

    module_invoke_all('nodejschat_' . $op, $nodejschat);
    module_invoke_all('entity_' . $op, $nodejschat, 'nodejschat');

    // Clear internal properties.
    unset($nodejschat->is_new);
    // Clear the static loading cache.
    entity_get_controller('nodejschat')->resetCache(array($nodejschat->cid));

    // Ignore slave server temporarily to give time for the
    // saved nodejschat to be propagated to the slave.
    db_ignore_slave();
  }
  catch (Exception $e) {
    $transaction->rollback();
    watchdog_exception('nodejschat', $e);
    throw $e;
  }
}

/**
 * Deletes a nodejschat.
 *
 * @param $cid
 *   A nodejschat ID.
 */
function nodejschat_delete($cid) {
  nodejschat_delete_multiple(array($cid));
}

/**
 * Deletes multiple nodejschats.
 *
 * @param $cids
 *   An array of nodejschat IDs.
 */
function nodejschat_delete_multiple($cids) {
  $transaction = db_transaction();
  if (!empty($cids)) {
    $nodejschats = nodejschat_load_multiple($cids, array());

    try {
      foreach ($nodejschats as $cid => $nodejschat) {
        // Call the node-specific callback (if any):
        module_invoke_all('nodejschat_delete', $nodejschat);
        module_invoke_all('entity_delete', $nodejschat, 'nodejschat');
        field_attach_delete('nodejschat', $nodejschat);

        // Remove this node from the search index if needed.
        // This code is implemented in node module rather than in search module,
        // because node module is implementing search module's API, not the other
        // way around.
        if (module_exists('search')) {
          search_reindex($cid, 'nodejschat');
        }
      }

      // Delete after calling hooks so that they can query node tables as needed.
      db_delete('nodejschat')
        ->condition('cid', $cids, 'IN')
        ->execute();
      db_delete('nodejschat_user')
        ->condition('cid', $cids, 'IN')
        ->execute();
      db_delete('nodejschat_role')
        ->condition('cid', $cids, 'IN')
        ->execute();
      db_delete('nodejschat_msg')
        ->condition('cid', $cids, 'IN')
        ->execute();
    }
    catch (Exception $e) {
      $transaction->rollback();
      watchdog_exception('nodejschat', $e);
      throw $e;
    }

    // Clear the page and block and node_load_multiple caches.
    entity_get_controller('nodejschat')->resetCache();
  }
}

/**
 * Load the entire nodejschat by cid.
 *
 * @param $cid
 *   The identifying comment id.
 * @param $reset
 *   Whether to reset the internal static entity cache. Note that the static
 *   cache is disabled in comment_entity_info() by default.
 *
 * @return
 *   The comment object.
 */
function nodejschat_load($cid, $reset = FALSE) {
  $nodejschat = nodejschat_load_multiple(array($cid), array(), $reset);
  return $nodejschat ? $nodejschat[$cid] : FALSE;
}

/**
 * Load nodejschats from the database.
 *
 * @param $cids
 *   An array of nodejschat IDs.
 * @param $conditions
 *   (deprecated) An associative array of conditions on the {nodejschats}
 *   table, where the keys are the database fields and the values are the
 *   values those fields must have. Instead, it is preferable to use
 *   EntityFieldQuery to retrieve a list of entity IDs loadable by
 *   this function.
 * @param $reset
 *   Whether to reset the internal static entity cache. Note that the static
 *   cache is disabled in nodejschat_entity_info() by default.
 *
 * @return
 *   An array of comment objects, indexed by nodejschat ID.
 *
 * @see entity_load()
 * @see EntityFieldQuery
 *
 * @todo Remove $conditions in Drupal 8.
 */
function nodejschat_load_multiple($cids = array(), $conditions = array(), $reset = FALSE) {
  return entity_load('nodejschat', $cids, $conditions, $reset);
}

/**
 * Get chat from session id
 */
function nodejschat_load_by_sid($sid = '') {
  if (empty($sid)) {
    $sid = session_id();
  }
  $nodejschats = nodejschat_load_multiple(array(), array('sid' => $sid));
  return reset($nodejschats);
  return FALSE;
}

/**
 * Get chat from user
 */
function nodejschat_load_by_user($uid) {
  $query = db_select("nodejschat_user", 'u');
  $query->fields('u', 'cid');
  $query->condition('uid', $uid);
  $nodejschats = $query->execute()->fetchCol(0);
  if ($nodejschats) {
    $items = array();
    foreach ($nodejschats as $cid) {
      $items[$cid] = nodejschat_load($cid);
    }
    return $items;
  }
  return FALSE;
}

/**
 * Builds a structured array representing the nodejschat's content.
 *
 * The content built for the nodejschat (field values, nodejschats, file attachments or
 * other nodejschat components) will vary depending on the $view_mode parameter.
 *
 * @param $nodejschat
 *   A nodejschat object.
 * @param $view_mode
 *   View mode, e.g. 'full', 'teaser'...
 * @param $langcode
 *   (optional) A language code to use for rendering. Defaults to the global
 *   content language of the current request.
 */
function nodejschat_build_content($nodejschat, $view_mode = 'full', $langcode = NULL) {
  if (!isset($langcode)) {
    $langcode = $GLOBALS['language_content']->language;
  }

  // Remove previously built content, if exists.
  $nodejschat->content = array();

  // Allow modules to change the view mode.
  $context = array(
    'entity_type' => 'nodejschat',
    'entity' => $nodejschat,
    'langcode' => $langcode,
  );
  drupal_alter('entity_view_mode', $view_mode, $context);

  // Build fields content.
  // In case of a multiple view, nodejschat_view_multiple() already ran the
  // 'prepare_view' step. An internal flag prevents the operation from running
  // twice.
  field_attach_prepare_view('nodejschat', array($nodejschat->cid => $nodejschat), $view_mode, $langcode);
  entity_prepare_view('nodejschat', array($nodejschat->cid => $nodejschat), $langcode);
  $nodejschat->content += field_attach_view('nodejschat', $nodejschat, $view_mode, $langcode);

  // Allow modules to make their own additions to the nodejschat.
  module_invoke_all('nodejschat_view', $nodejschat, $view_mode, $langcode);
  module_invoke_all('entity_view', $nodejschat, 'nodejschat', $view_mode, $langcode);

  // Make sure the current view mode is stored if no module has already
  // populated the related key.
  $nodejschat->content += array('#view_mode' => $view_mode);
}


/**
 * Generates an array for rendering the given nodejschat.
 */
function nodejschat_view($nodejschat, $view_mode = 'full', $langcode = NULL) {
  if (!isset($langcode)) {
    $langcode = $GLOBALS['language_content']->language;
  }

  // Populate $node->content with a render() array.
  nodejschat_build_content($nodejschat, $view_mode, $langcode);

  $build = $nodejschat->content;

  unset($nodejschat->content);
  // We don't need duplicate rendering info in node->content.

  $build['board'] = array(
    '#weight' => 0
  );

  if ($messages = nodejschat_load_latest_messages($nodejschat, $nodejschat->settings['msg_amount'])) {
    $build['board'] += nodejschat_message_view_multiple($messages, $nodejschat);;
    $build['board']['#messages'] = $messages;
  }

  if (($nodejschat->status != NODEJSCHAT_ARCHIVED) && nodejschat_user_access($nodejschat, 'write')) {
    $build['entry'] = array(
      '#theme' => 'nodejschat_entry',
      '#weight' => $build['board']['#weight'] + 1,
      'entry_form' => drupal_get_form('nodejschat_entry_form_' . $nodejschat->cid, $nodejschat)
    );
  }

  // Build settings
  $cid = $nodejschat->cid;

  $build += array(
    '#theme' => 'nodejschat',
    '#nodejschat' => $nodejschat,
    '#attached' => array(
      'js' => array(array('data' => drupal_get_path('module', 'nodejschat') . '/js/nodejschat.js', 'weight' => 10)),
      'css' => array()
    )
  );

  //Add theme
 if (!empty($nodejschat->settings['theme'])) {
    $theme = nodejschat_load_themes($nodejschat->settings['theme']);
    if (!empty($theme['css'])) {
      $build['#attached']['css'] = array_merge($build['#attached']['css'], $theme['css']);
    }
    if (!empty($theme['js'])) {
      $build['#attached']['js'] = array_merge($build['#attached']['js'], $theme['js']);
    }
  }

  // Js Settings to client
  $previous_message_id = nodejschat_get_previous_message_cmid($nodejschat, $nodejschat->settings['msg_amount']);
  $js_settings['nodejschat']['chats'][$cid] = array(
    'cid' => $cid,
    'title' => $nodejschat->title,
    'latestMsgId' => $nodejschat->latest_message ? $nodejschat->latest_message->cmid : 0,
    'prevMsgId' => $previous_message_id ? $previous_message_id : 0,
    'limit' => $nodejschat->settings['msg_amount']
  );

  drupal_add_js($js_settings, 'setting');

  // Allow modules to modify the structured nodejschat.
  if (!empty($nodejschat->cid) && !($view_mode == 'full')) {
    $build['#contextual_links']['nodejschat'] = array('nodejschat', array($nodejschat->cid));
  }

  // Allow modules to modify the structured node.
  $type = 'nodejschat';
  drupal_alter(array('nodejschat_view', 'entity_view'), $build, $type);
  //Send a content channel token to Node.js.
  nodejs_send_content_channel_token('nodejschat_' . $cid, TRUE);

  return $build;
}

/**
 * Constructs a drupal_render() style array from an array of loaded nodejschats.
 *
 * @param $nodejschats
 *   An array of nodejschats as returned by nodejschat_load_multiple().
 * @param $view_mode
 *   View mode, e.g. 'full', 'teaser'...
 * @param $weight
 *   An integer representing the weight of the first nodejschat in the list.
 * @param $langcode
 *   (optional) A language code to use for rendering. Defaults to NULL which is
 *   the global content language of the current request.
 *
 * @return
 *   An array in the format expected by drupal_render().
 */
function nodejschat_view_multiple($nodejschats, $view_mode = 'teaser', $weight = 0, $langcode = NULL) {
  field_attach_prepare_view('nodejschat', $nodejschats, $view_mode, $langcode);
  entity_prepare_view('nodejschat', $nodejschats, $langcode);
  $build = array();
  foreach ($nodejschats as $nodejschat) {
    $build['nodejschats'][$nodejschat->cid] = nodejschat_view($nodejschat, $view_mode, $langcode);
    $build['nodejschats'][$nodejschat->cid]['#weight'] = $weight;
    $weight++;
  }
  $build['nodejschats']['#sorted'] = TRUE;
  return $build;
}

/**
 * Get the message total for a chat.
 */
function nodejschat_total_messages($nodejschat) {
  if ($nodejschat->cid) {
    return db_select('nodejschat_msg', 'cm')
      ->fields('cm')
      ->condition('cid', $nodejschat->cid)
      ->execute()
      ->rowCount();
  }
  return 0;
}

/**
 * Write a message into a nodejschat, and allow any interested modules to react.
 *
 * @param array $message
 * @return boolean
 */
function nodejschat_save_message($nodejschat, $message) {
  // Allow modules to change the messages before saved.
  drupal_alter('nodejschat_msg', $nodejschat, $message);
  if (drupal_write_record('nodejschat_msg', $message)) {
    foreach (module_implements('nodejschat_msg_saved') as $module) {
      $function = $module . '_nodejschat_msg_saved';
      $function($nodejschat, $message);
    }
    return $message['cmid'];
  }
  return FALSE;
}

/**
 * Load older messages.
 *
 */
function nodejschat_get_previous_messages($nodejschat, $cmid, $limit = 20) {
  $query = db_select('nodejschat_msg', 'm');
  $query->leftJoin('users', 'u', 'u.uid = m.uid');
  $query->fields('m')->fields('u')
    ->condition('cid', $nodejschat->cid)
    ->condition('cmid', $cmid, '<')
    ->orderBy('m.cmid', 'DESC');
  $query = $query->extend('PagerDefault')->limit($limit);
  $messages = array();
  foreach ($query->execute() as $message) {
    $messages[$message->cmid] = $message;
  }
  return $messages;
}

/**
 * Get messages for a given chat.
 *
 * @param $last_cmid
 *   Only load messages with cmids greater than this value.
 * @return array $messages
 */
function nodejschat_get_messages($nodejschat, $order = NODEJSCHAT_MSG_ASC, $limit = 20) {
  $query = db_select('nodejschat_msg', 'm');
  $query->leftJoin('users', 'u', 'u.uid = m.uid');
  $query->fields('m')->fields('u')
    ->condition('cid', $nodejschat->cid)
    ->orderBy('m.cmid', $order);
  $query = $query->extend('PagerDefault')->limit($limit);
  $messages = array();
  foreach ($query->execute() as $message) {
    $messages[$message->cmid] = $message;
  }
  return $messages;
}

/**
 * Load the latest messages.
 *
 * @param $limit
 *   Number of messages to load.
 * @return array
 */
function nodejschat_load_latest_messages($nodejschat, $limit = 20) {
  if (!preg_match('/^\d+$/', $limit)) {
    return array();
  }
  $messages = nodejschat_get_messages($nodejschat, NODEJSCHAT_MSG_DESC, $limit);
  return array_reverse($messages, TRUE);
}

/**
 * Load the cmid of the oldest displayed message.
 *
 * @return $cmid
 */
function nodejschat_get_previous_message_cmid($nodejschat, $limit = 20) {
  $messages = nodejschat_load_latest_messages($nodejschat, $limit);
  $message = array_shift($messages);
  if (isset($message->cmid)) {
    return $message->cmid;
  }
  return 0;
}

/**
 * Generates an array for rendering the given message.
 * @param $message
 *   A message object.
 * @param $nodejschat
 *   A nodejschat object.
 */
function nodejschat_message_view($message, $nodejschat, $langcode = NULL) {
  if (!isset($langcode)) {
    $langcode = $GLOBALS['language_content']->language;
  }
  // Remove previously built content, if exists.
  $message->content = array();
  // Populate $message->content with a render() array.

  $message->content['message'] = array(
    '#markup' => check_markup($message->msg, $nodejschat->format)
  );
  $message->content += array(
    'username' => nodejschat_message_get_username($message),
    'datetime' => nodejschat_message_time_string($nodejschat->settings['msg_date_format'], $message->modified)
  );

  module_invoke_all('nodejschat_message_view', $message, $nodejschat);

  $build = $message->content;

  unset($message->content);

  $build += array(
    '#theme' => 'nodejschat_message',
    '#nodejschat' => $nodejschat,
    '#message' => $message,
    '#weight' => $message->cmid,
    '#language' => $langcode
  );

  // Allow modules to modify the structured message.
  drupal_alter(array('nodejschat_message_view'), $build);

  return $build;
}

/**
 * Constructs a drupal_render() style array from an array of loaded nodejschats.
 *
 * @param $nodejschats
 *   An array of nodejschats as returned by nodejschat_load_multiple().
 * @param $view_mode
 *   View mode, e.g. 'full', 'teaser'...
 * @param $weight
 *   An integer representing the weight of the first nodejschat in the list.
 * @param $langcode
 *   (optional) A language code to use for rendering. Defaults to NULL which is
 *   the global content language of the current request.
 *
 * @return
 *   An array in the format expected by drupal_render().
 */
function nodejschat_message_view_multiple($messages, $nodejschat, $langcode = NULL) {
  $build = array();
  foreach ($messages as $message) {
    $build['messages'][$message->cmid] = nodejschat_message_view($message, $nodejschat, $langcode);
    $build['messages'][$message->cmid]['#weight'] = $message->cmid;
  }
  $build['messages']['#sorted'] = TRUE;
  return $build;
}


/**
 * Check to see if the given user has access to this chat.
 *
 * @param mixed $account
 * @param mixed $access_type
 * @return boolean
 */
function nodejschat_user_access($nodejschat, $access_type) {
  global $user;
  // Allow modules to take over access to nodejschats. To avoid ambiguity,
  // if any modules returns TRUE or FALSE rather than NULL, they win.
  foreach (module_implements('nodejschat_access') as $module) {
    $function = $module . '_nodejschat_access';
    $access = $function($nodejschat, $user, $access_type);
    if ($access === TRUE || $access === FALSE) {
      return $access;
    }
  }

  // No module cares about access to this nodejschat, run our standard checks.
  $admin_role_rid = variable_get('user_admin_role', FALSE);
  if ($user->uid == 1 || ($admin_role_rid && isset($user->roles[$admin_role_rid]))) {
    return TRUE;
  }

  $params = array(
    ':cid' => $nodejschat->cid,
    ':uid' => $user->uid,
  );
  $sql = 'SELECT cid, read_only FROM {nodejschat_user} WHERE cid = :cid AND uid = :uid';
  if ($user_access = db_query($sql, $params)->fetchObject()) {
    if (!$user_access->read_only || $access_type == 'read') {
      return TRUE;
    }
  }

  if ($allowed_roles = $nodejschat->allowed_roles) {
    foreach ($allowed_roles as $rid => $role_access) {
      if (isset($user->roles[$rid])) {
        if (!$role_access->read_only || $access_type == 'read') {
          return TRUE;
        }
      }
    }
  }

  if ($access_type == 'read' && $nodejschat->public || user_access('access all nodejschats', $user)) {
    return TRUE;
  }
}

/**
 * Update the roles allowed to access a chat.
 *
 * @param mixed $rids
 * @return void
 */
function nodejschat_update_rids($nodejschat, $rids) {
  $sql = "DELETE FROM {nodejschat_role} WHERE cid = :cid";
  db_query($sql, array(':cid' => $nodejschat->cid));
  foreach ($rids as $rid) {
    $data = array('rid' => $rid->rid, 'cid' => $nodejschat->cid, 'read_only' => $rid->read_only);
    drupal_write_record('nodejschat_role', $data);
  }
}

/**
 * Update the uids allowed to access a chat.
 *
 * @param mixed $uids
 * @return void
 */
function nodejschat_update_users($nodejschat, $uids) {
  foreach ($uids as $uid) {
    $count = db_select('nodejschat_user', 'u')
      ->fields('u')
      ->condition('uid', $uid->uid)
      ->condition('cid', $nodejschat->cid)
      ->execute()->rowCount();
    $data = array('uid' => $uid->uid, 'cid' => $nodejschat->cid, 'read_only' => $uid->read_only, 'modified' => $uid->modified);
    if (!$count) {
      drupal_write_record('nodejschat_user', $data);
    }
    else {
      drupal_write_record('nodejschat_user', $data, array('uid', 'cid'));
    }
  }
}

/**
 * Controller class for nodejschat.
 *
 * This extends the DrupalDefaultEntityController class, adding required
 * special handling for comment objects.
 */
class NodejsChatController extends DrupalDefaultEntityController {
  protected function attachLoad(&$nodejschats, $revision_id = FALSE) {
    // Setup standard comment properties.
    foreach ($nodejschats as $key => $nodejschat) {
      $nodejschats[$key]->settings = array();
      $nodejschats[$key]->allowed_rids = array();
      $nodejschats[$key]->allowed_uids = array();
      $nodejschats[$key]->total_messages = nodejschat_total_messages($nodejschat);
      $nodejschats[$key]->latest_message = NULL;
      // Add any additional roles from the database.
      $roles = db_query('SELECT rid, read_only FROM {nodejschat_role} WHERE cid = :cid', array(':cid' => $nodejschat->cid))->fetchAll();
      foreach ($roles as $role) {
        $nodejschats[$nodejschat->cid]->allowed_rids[$role->rid] = $role;
      }
      // Add any additional nodejschats from the database.
      $uids = db_query('SELECT uid, read_only, modified FROM {nodejschat_user} WHERE cid = :cid', array(':cid' => $nodejschat->cid))->fetchAll();
      foreach ($uids as $uid) {
        $nodejschats[$nodejschat->cid]->allowed_uids[] = $uid;
      }

      $latest_messages = nodejschat_get_messages($nodejschat, NODEJSCHAT_MSG_DESC, 1);
      $nodejschats[$key]->latest_message = $latest_messages ? current($latest_messages) : NULL;

      $nodejschats[$key]->settings = variable_get('nodejschat_default_settings', array());
    }
    // Call the default attachLoad() method. This will add fields and call
    // hook_nodejschat_load().
    parent::attachLoad($nodejschats, $revision_id);
  }
}
